feat!: streaming SSR and live! + emit! regions - #373
Open
pikaju wants to merge 123 commits into
Open
Conversation
pikaju
marked this pull request as ready for review
September 1, 2026 15:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR rebuilds the view layer around a lazy
Viewtrait and puts streaming on top of it: pages can now send what they have right away and stream the slow parts in when they finish, over the same response and without any client-side fetching.Views are now lazy
view!no longer renders eagerly into aResult<View>. It evaluates to a plain value implementing the newViewtrait, and rendering happens later, when the router turns the view into a response. Handlers and components returnResult<impl View>:Follow-on API changes:
Slot<'_>instead ofslot: Result, and interpolate it directly with(slot).Child<'_>type instead ofView, usually declared#[default]so the component can be called without children.#[component(boxed)]is gone. Recursive components break the type cycle with.boxed()from the newViewExttrait, which also providesfirst()andsingle()for resolving a view by hand.AsyncIntoResponsetrait (everyIntoResponsetype implements it for free), so a response can await the view's first content before the status line goes out.live!andemit!live!marks a region of the page whose content can still change while the response streams. Its body is ordinary async Rust;emit!renders markup into the region, and every emission replaces the previous one in the browser:The heading and the loading message reach the browser immediately; once the quote is ready it replaces the loading message in place. The swap needs no client library: the response carries a small script, regions are delimited by marker comments with per-request ids, and each swap arrives as a template the script splices in.
emit!evaluates to aResult<EmitToken>and the body returns one, so the type system reminds you that a region must emit at least once and lets the body handle a failed emission (retry, fall back) instead of ending the stream. A live region is a view like any other: components can return one, take one as child content, and several regions on a page stream independently.suspenseanderror_boundaryThe two most common live-region shapes come prepackaged as built-in components, both thin wrappers over
live!/emit!:suspenseshows the fallback until its child content is ready.error_boundaryrenders its child and swaps in the fallback view when any part of it fails; returningErrfrom the fallback rethrows the error up the view tree. This replaces the old pattern of matching onslot: Resultin a layout: a layout now wraps its slot in anerror_boundary, downcasts the error, and can still set the status code from the fallback view.Streaming-aware error and redirect handling
Once a response has started streaming, its status line is fixed. An error that escapes after that point ends the stream, and a redirect raised mid-stream is delivered as a client-side navigation (an injected
window.location.replacescript) instead of aLocationheader.